package com.jq.licodeandroidclient;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.AudioTrack;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;

import android.app.Activity;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Base64;
import android.util.Log;

import com.jq.licodeandroidclient.apprtc.VideoStreamsView;
import com.koushikdutta.async.http.AsyncHttpClient;
import com.koushikdutta.async.http.WebSocket;
import com.koushikdutta.async.http.socketio.Acknowledge;
import com.koushikdutta.async.http.socketio.ConnectCallback;
import com.koushikdutta.async.http.socketio.EventCallback;
import com.koushikdutta.async.http.socketio.SocketIOClient;
import com.koushikdutta.async.http.socketio.SocketIORequest;
import com.koushikdutta.async.http.socketio.transport.SocketIOTransport;

/**
 * A simple class to connect to a licode server and provides callbacks for the
 * standard events associated with this.
 */
public class LicodeConnector implements VideoConnectorInterface {
    /**
     * flag to store if basic initialization has happened
     */
    private static boolean sInitializedAndroidGlobals;
    /**
     * socket.io client
     */
    volatile SocketIOClient mIoClient = null;
    /**
     * lock object for socket communication
     */
    private Object mSocketLock = new Object();
    /**
     * current state of the connection
     */
    volatile State mState = State.kUninitialized;
    /**
     * description of the turn server, username, password, and url
     */
    JSONObject mTurnServer;
    /**
     * stun server url
     */
    String mStunServerUrl;
    /**
     * default video bandwidth
     */
    int mDefaultVideoBW;
    /**
     * max video bandwidth
     */
    int mMaxVideoBW = 75;
    /**
     * max audio bandwidth
     */
    int mMaxAudioBW = 25;
    /**
     * list of the streams
     */
    ConcurrentHashMap<String, StreamDescriptionInterface> mRemoteStream = new ConcurrentHashMap<String, StreamDescriptionInterface>();
    /**
     * list of the streams
     */
    HashMap<String, StreamDescription> mLocalStream = new HashMap<String, StreamDescription>();
    /**
     * current room id
     */
    String mRoomId;
    /**
     * list of all current observers
     */
    ConcurrentLinkedQueue<RoomObserver> mObservers = new ConcurrentLinkedQueue<LicodeConnector.RoomObserver>();
    /**
     * local video stream
     */
    private VideoSource mVideoSource;
    /**
     * local video capturer
     */
    private VideoCapturer mVideoCapturer;
    /**
     * if local video stream was paused
     */
    private boolean mVideoStopped = false;
    /**
     * factory for peer connections
     */
    private static PeerConnectionFactory sFactory;
    /**
     * list of stun and turn servers available for all connections
     */
    volatile ArrayList<PeerConnection.IceServer> mIceServers = new ArrayList<PeerConnection.IceServer>();
    /**
     * the handler for the special video chat thread
     */
    private static Handler sVcHandler = null;
    /**
     * special lock object when accessing the vc handler instance
     */
    private static Object sVcLock = new Object();
    /**
     * server confirmed rights
     */
    private boolean mPermissionPublish, mPermissionSubscribe;

    /**
     * helper class - runnable that can be cancelled
     */
    private static interface CancelableRunnable extends Runnable {
        /**
         * cancels the runnable
         */
        void cancel();
    }

    /**
     * refresh token runnable
     */
    private CancelableRunnable mRefreshTokenRunnable;

    /**
     * may or may not provide logging output - as desired
     */
    static void log(String s) {
        // TODO dk: logging?!
        System.out.println(s);
    }

    EventCallback mOnAddStream = new EventCallback() {
        @Override
        public void onEvent(JSONArray args, Acknowledge ack) {
            // [{"data":true,"id":331051653483882560,"screen":"","audio":true,"video":true}]
            log("mOnAddStream");

            try {
                StreamDescription stream = StreamDescription.parseJson(args
                        .getJSONObject(0));

                boolean isLocal = mLocalStream.get(stream.getId()) != null;
                if (!isLocal) {
                    mRemoteStream.put(stream.getId(), stream);
                    triggerStreamAdded(stream);
                }
            } catch (JSONException e) {
            }
        }
    };
    EventCallback mOnSubscribeP2P = new EventCallback() {
        @Override
        public void onEvent(JSONArray args, Acknowledge ack) {
            // not yet relevant
        }
    };
    EventCallback mOnPublishP2P = new EventCallback() {
        @Override
        public void onEvent(JSONArray args, Acknowledge ack) {
            // not yet relevant
        }
    };
    EventCallback mOnDataStream = new EventCallback() {
        @Override
        public void onEvent(JSONArray args, Acknowledge ack) {
            log("mOnDataStream");

            try {
                JSONObject param = args.getJSONObject(0);
                String streamId = param.getString("id");
                String message = param.getString("msg");
                StreamDescriptionInterface stream = mRemoteStream.get(streamId);
                for (RoomObserver obs : mObservers) {
                    obs.onStreamData(message, stream);
                }
            } catch (JSONException e) {
            }
        }
    };
    EventCallback mOnRemoveStream = new EventCallback() {
        @Override
        public void onEvent(JSONArray args, Acknowledge ack) {
            // [{"id":331051653483882560}]
            log("mOnRemoveStream");

            try {
                JSONObject param = args.getJSONObject(0);
                String streamId = param.getString("id");
                StreamDescription stream = (StreamDescription) mRemoteStream
                        .get(streamId);

                if (stream != null) {
                    removeStream(stream);
                    mRemoteStream.remove(streamId);
                    triggerStreamRemoved(stream);
                }
            } catch (JSONException e) {
            }
        }
    };
    EventCallback mDisconnect = new EventCallback() {
        @Override
        public void onEvent(JSONArray args, Acknowledge ack) {
            log("mDisconnect");
            disconnect();
        }
    };

    /**
     * peer connection observer
     */
    private class MyPcObserver implements PeerConnection.Observer {
        /**
         * the associated sdp observer
         */
        private LicodeSdpObserver mSdpObserver;
        /**
         * stream description
         */
        private StreamDescriptionInterface mDesc;

        public MyPcObserver(LicodeSdpObserver observer,
                            StreamDescriptionInterface desc) {
            mSdpObserver = observer;
            mDesc = desc;
        }

        public LicodeSdpObserver getSdpObserver() {
            return mSdpObserver;
        }

        @Override
        public void onSignalingChange(PeerConnection.SignalingState arg0) {
        }

        @Override
        public void onRemoveStream(MediaStream arg0) {
            // stream gone?
        }

        @Override
        public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatherState) {
            if (iceGatherState == PeerConnection.IceGatheringState.COMPLETE) {
                mSdpObserver.iceReady();
            }
        }

        @Override
        public void onIceConnectionChange(PeerConnection.IceConnectionState arg0) {
        }

        @Override
        public void onIceCandidate(IceCandidate iceCandidate) {
        }

        @Override
        public void onError() {
            log("PeerConenctionObserver.onError");
        }

        @Override
        public void onDataChannel(DataChannel arg0) {
        }

        @Override
        public void onAddStream(final MediaStream media) {
            if (mSdpObserver.isLocal()) {
                return;
            }
            if (media.videoTracks.size() == 1 && mDesc != null) {
                ((StreamDescription) mDesc).setMedia(media);
                triggerMediaAvailable(mDesc);
            }

        }

        @Override
        public void onRenegotiationNeeded() {
            log("PeerConnectionObserver.onRenegotiationNeeded");
        }
    }

    ;

    /**
     * context/activity
     */
    private volatile Activity mActivity;
    /**
     * local media stream
     */
    private MediaStream lMS;
    /**
     * the currently active nick
     */
    private String mNick;

    public LicodeConnector() {
    }

    @Override
    public void onPause() {
        sVcHandler.post(new Runnable() {
            @Override
            public void run() {
                if (mVideoSource != null) {
                    mVideoSource.stop();
                    mVideoStopped = true;
                }
            }
        });
    }

    @Override
    public void onResume() {
        sVcHandler.post(new Runnable() {
            @Override
            public void run() {
                if (mVideoSource != null && mVideoStopped) {
                    mVideoSource.restart();
                    mVideoStopped = false;
                }
            }
        });
    }

    @Override
    public State getState() {
        return mState;
    }

    @Override
    public boolean isConnected() {
        return mState == State.kConnected || mState == State.kConnecting;
    }

    @Override
    public void init(Activity context, String nick) {
        synchronized (sVcLock) {
            if (sVcHandler == null) {
                HandlerThread vcthread = new HandlerThread(
                        "LicodeConnectorThread");
                vcthread.start();
                sVcHandler = new Handler(vcthread.getLooper());
            }
        }
        if (context == null) {
            throw new NullPointerException(
                    "Failed to initialize LicodeConnector. Activity is required.");
        }
        mActivity = context;
        mState = State.kDisconnected;
        mNick = nick;

        Runnable init = new Runnable() {
            @Override
            public void run() {
                if (!sInitializedAndroidGlobals) {
                    sInitializedAndroidGlobals = true;
                    // newer libjingle versions have options for video and audio
                    PeerConnectionFactory.initializeAndroidGlobals(mActivity);// ,
                    // true,
                    // true);
                }

                if (sFactory == null) {
                    sFactory = new PeerConnectionFactory();
                }

            }

            ;
        };
        sVcHandler.post(init);
    }

    @Override
    public void setBandwidthLimits(int video, int audio) {
        mMaxVideoBW = video;
        mMaxAudioBW = audio;
    }

    @Override
    public void connect(final String token) {
        if (mState == State.kUninitialized) {
            return;
        }
        if (isConnected()) {
            return;
        }

        mState = State.kConnecting;
        mActivity.runOnUiThread(new Runnable() {

            @Override
            public void run() {
                createToken(token);
            }
        });
    }

    /**
     * sends a token - when required
     */
    public void refreshVideoToken(String token) {
        token = LicodeConnector.decodeToken(token);
        if (token == null) {
            return;
        }

        try {
            JSONObject jsonToken = new JSONObject(token);
//			handleTokenRefresh(jsonToken);

            sendMessageSocket("refreshToken", jsonToken, new Acknowledge() {
                @Override
                public void acknowledge(JSONArray arg0) {
                    // read publish right from result
                    log("Refresh token Acknowledge: " + arg0.toString());
                    parseVideoTokenResponse(arg0);

                    if (mPermissionPublish) {
                        triggerPublishAllowed();
                    } else {
                        unpublish();
                    }
                }
            });
        } catch (JSONException e) {
        }
    }

    @Override
    public void disconnect() {
        if (mState == State.kUninitialized || mState == State.kDisconnected
                || mState == State.kDisconnecting) {
            return;
        }
        if (mState == State.kConnecting) {
            // TODO dk: figure out how to handle this!
        }

        sVcHandler.post(new Runnable() {
            @Override
            public void run() {
                doDisconnect();
            }
        });
    }

    /**
     * handle actual disconnecting - from ui thread only
     */
    void doDisconnect() {
        mState = State.kDisconnecting;
        for (RoomObserver obs : mObservers) {
            obs.onRoomDisconnected();
        }
        Set<String> keyset = mRemoteStream.keySet();
        for (String key : keyset) {
            StreamDescription stream = (StreamDescription) mRemoteStream
                    .get(key);
            removeStream(stream);
            triggerStreamRemoved(stream);
        }
        mRemoteStream.clear();

        if (mLocalStream.size() > 0) {
            unpublish();
        }

        synchronized (mSocketLock) {
            if (mIoClient != null) {
                mIoClient.disconnect();
                mIoClient = null;
            }
        }

        mState = State.kDisconnected;
    }

    /**
     * handles time based refreshing of tokens - when they have a duration
     */
    void handleTokenRefresh(JSONObject jsonToken) {
        int duration = 0;

        try {
            duration = jsonToken.getInt("duration");
        } catch (JSONException e) {
            e.printStackTrace();
        }

        if (duration > 0) {
            if (mRefreshTokenRunnable != null) {
                mRefreshTokenRunnable.cancel();
            }
            mRefreshTokenRunnable = new CancelableRunnable() {
                /**
                 * keeps track if this is still to be run, or has been cancelled
                 */
                private volatile boolean mIsActive = true;

                @Override
                public void run() {
                    if (!mIsActive) {
                        return;
                    }

                    triggerRequestVideoToken();
                }

                @Override
                public void cancel() {
                    mIsActive = false;
                }
            };
            long refreshTime = duration - 10;
            if (refreshTime < 1) {
                refreshTime = 1;
            }
            sVcHandler.postDelayed(mRefreshTokenRunnable, refreshTime * 1000L);
        }
    }

    /**
     * decodes a video token into a string which can then be turned into a json
     * object, returns null on errors
     */
    private static final String decodeToken(String result) {
        try {
            String token = new String(Base64.decode(result.getBytes(),
                    Base64.DEFAULT), "UTF-8");
            log("Licode token decoded: " + token);
            return token;
        } catch (UnsupportedEncodingException e) {
            log("Failed to decode token: " + e.getMessage());
        }
        return null;
    }

    /**
     * called with the connection token
     */
    void createToken(String result) {
        if (result == null) {
            Log.d("Licode","token 获取失败");
            return;
        }
        String token = LicodeConnector.decodeToken(result);
        if (token == null) {
            return;
        }

        try {
            mRemoteStream.clear();
            final JSONObject jsonToken = new JSONObject(token);
            String host = jsonToken.getString("host");
            if (!host.startsWith("http://")) {
                host = "http://" + host;
            }
//			handleTokenRefresh(jsonToken);

            SocketIOClient.connect(AsyncHttpClient.getDefaultInstance(), host,
                    new ConnectCallback() {
                        @Override
                        public void onConnectCompleted(Exception err,
                                                       SocketIOClient client) {
                            if (err != null) {
                                err.printStackTrace();
                                return;
                            }

                            try {
                                // workaround - 2nd connection event
                                JSONObject jsonParam = new JSONObject();
                                jsonParam.put("reconnect", false);
                                jsonParam.put("secure",
                                        jsonToken.getBoolean("secure"));
                                jsonParam.put("force new connection", true);

                                JSONArray arg = new JSONArray();
                                arg.put(jsonParam);
                                client.emit("connection", arg);
                                log("Licode: Connection established!");
                            } catch (JSONException e) {
                                e.printStackTrace();
                            }
                            synchronized (mSocketLock) {
                                mIoClient = client;
                                client.on("onAddStream", mOnAddStream);
                                client.on("onSubscribeP2P", mOnSubscribeP2P);
                                client.on("onPublishP2P", mOnPublishP2P);
                                client.on("onDataStream", mOnDataStream);
                                client.on("onRemoveStream", mOnRemoveStream);
                                client.on("disconnect", mDisconnect);
                            }

                            sendMessageSocket("token", jsonToken,
                                    new Acknowledge() {
                                        @Override
                                        public void acknowledge(JSONArray result) {
                                            log("Licode: createToken -> connect");
                                            log(result.toString());
                                            try {
                                                // ["success",{"maxVideoBW":300,"id":"5384684c918b864466c853d6","streams":[],"defaultVideoBW":300,"turnServer":{"password":"","username":"","url":""},"stunServerUrl":"stun:stun.l.google.com:19302"}]
                                                // ["success",{"maxVideoBW":300,"id":"5384684c918b864466c853d6","streams":[{"data":true,"id":897203996079042600,"screen":"","audio":true,"video":true},{"data":true,"id":841680482029914900,"screen":"","audio":true,"video":true}],"defaultVideoBW":300,"turnServer":{"password":"","username":"","url":""},"stunServerUrl":"stun:stun.l.google.com:19302"}]
                                                if ("success"
                                                        .equalsIgnoreCase(result
                                                                .getString(0)) == false) {
                                                    return;
                                                }

                                                JSONObject jsonObject = result
                                                        .getJSONObject(1);
                                                parseVideoTokenResponse(result);

                                                if (jsonObject
                                                        .has("turnServer")) {
                                                    mTurnServer = jsonObject
                                                            .getJSONObject("turnServer");
                                                    String url = mTurnServer
                                                            .getString("url");
                                                    String usr = mTurnServer
                                                            .getString("username");
                                                    String pwd = mTurnServer
                                                            .getString("password");
                                                    if (!url.isEmpty()) {
                                                        mIceServers
                                                                .add(new PeerConnection.IceServer(
                                                                        url,
                                                                        usr,
                                                                        pwd));
                                                    }
                                                }
                                                if (jsonObject
                                                        .has("stunServerUrl")) {
                                                    mStunServerUrl = jsonObject
                                                            .getString("stunServerUrl");
                                                    if (!mStunServerUrl
                                                            .isEmpty()) {
                                                        mIceServers
                                                                .add(new PeerConnection.IceServer(
                                                                        mStunServerUrl));
                                                    }
                                                }
                                                if (jsonObject
                                                        .has("defaultVideoBW")) {
                                                    mDefaultVideoBW = jsonObject
                                                            .getInt("defaultVideoBW");
                                                }
                                                if (jsonObject
                                                        .has("maxVideoBW")) {
                                                    mMaxVideoBW = jsonObject
                                                            .getInt("maxVideoBW");
                                                }

                                                mState = State.kConnected;

                                                // update room id
                                                mRoomId = jsonObject
                                                        .getString("id");

                                                for (RoomObserver obs : mObservers) {
                                                    obs.onRoomConnected(mRemoteStream);
                                                }

                                                // retrieve list of streams
                                                JSONArray streams = jsonObject
                                                        .getJSONArray("streams");
                                                for (int index = 0, n = streams
                                                        .length(); index < n; ++index) {
                                                    // {"data":true,"id":897203996079042600,"screen":"","audio":true,"video":true}
                                                    JSONObject arg = streams
                                                            .getJSONObject(index);
                                                    StreamDescription stream = StreamDescription
                                                            .parseJson(arg);
                                                    mRemoteStream.put(
                                                            stream.getId(),
                                                            stream);
                                                    triggerStreamAdded(stream);
                                                }
                                            } catch (JSONException e) {
                                            }
                                        }
                                    });
                        }
                    });
        } catch (JSONException e) {
            Log.d("Licode", "error");
        }
    }

    /**
     * send a json something on the specified channel via socket.io
     */
    void sendMessageSocket(String channel, Object param, Acknowledge ack) {
        synchronized (mSocketLock) {
            if (mIoClient == null) {
                return;
            }
            JSONArray jsonArgs = new JSONArray();
            jsonArgs.put(param);
            if (ack == null) {
                ack = new Acknowledge() {
                    @Override
                    public void acknowledge(JSONArray arg0) {
                        log("LicodeConnector: No one interested in response: "
                                + arg0.toString());
                    }
                };
            }
            mIoClient.emit(channel, jsonArgs, ack);
        }
    }

    void sendSDPSocket(String type, JSONObject param0, JSONObject param1,
                       Acknowledge ack) {
        synchronized (mSocketLock) {
            if (mIoClient == null) {
                return;
            }
            JSONArray jsonArgs = new JSONArray();
            jsonArgs.put(param0);
            jsonArgs.put(param1);
            mIoClient.emit(type, jsonArgs, ack);
        }
    }

    void sendSDPSocket(String type, JSONArray params, Acknowledge ack) {
        synchronized (mSocketLock) {
            if (mIoClient == null) {
                return;
            }
            mIoClient.emit(type, params, ack);
        }
    }

    void sendDataSocket(String streamId, String message) {
        JSONObject param = new JSONObject();
        try {
            param.put("id", streamId);
            param.put("msg", message);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        sendMessageSocket("sendDataStream", param, null);
    }

    void removeStream(StreamDescription stream) {
        stream.onClosing();
        triggerStreamRemoved(stream);
    }

    @Override
    public void unsubscribe(String streamId) {
        StreamDescription stream = (StreamDescription) mRemoteStream
                .get(streamId);

        if (stream != null) {
            disable(stream);
        }
    }

    @Override
    public void addObserver(final RoomObserver observer) {
        mObservers.add(observer);

        if (isConnected()) {
            mActivity.getWindow().getDecorView().post(new Runnable() {
                @Override
                public void run() {
                    observer.onRoomConnected(mRemoteStream);
                }
            });
        }
    }

    @Override
    public void removeObserver(RoomObserver observer) {
        mObservers.remove(observer);
    }

    /**
     * get access to the camera
     */
    private VideoCapturer getVideoCapturer() {
        String[] cameraFacing = {"front", "back"};
        int[] cameraIndex = {0, 1};
        int[] cameraOrientation = {0, 90, 180, 270};
        for (String facing : cameraFacing) {
            for (int index : cameraIndex) {
                for (int orientation : cameraOrientation) {
                    String name = "Camera " + index + ", Facing " + facing
                            + ", Orientation " + orientation;
                    VideoCapturer capturer = VideoCapturer.create(name);
                    if (capturer != null) {
                        log("Using camera: " + name);
                        return capturer;
                    }
                }
            }
        }
        throw new RuntimeException("Failed to open capturer");
    }

    // Implementation detail: bridge the VideoRenderer.Callbacks interface to
    // the
    // VideoStreamsView implementation.
    public static class VideoCallbacks implements VideoRenderer.Callbacks {
        private final VideoStreamsView view;
        private final String streamId;

        public VideoCallbacks(VideoStreamsView view, String streamId) {
            this.view = view;
            this.streamId = streamId;
        }

        @Override
        public void setSize(final int width, final int height) {
            view.queueEvent(new Runnable() {
                public void run() {
                    view.setSize(streamId, width, height);
                }
            });
        }

        @Override
        public void renderFrame(VideoRenderer.I420Frame frame) {
            view.queueFrame(streamId, frame);
        }
    }

    private class LicodeSdpObserver implements SdpObserver {
        /**
         * the sdp created locally
         */
        SessionDescription mLocalSdp = null;
        /**
         * whether or not this is a publish attempt
         */
        boolean mIsPublish = false;
        /**
         * the current signalling channel on socket.io
         */
        String mSignalChannel = "subscribe";
        /**
         * the associated stream
         */
        final StreamDescription mStream;
        /**
         * id of the offerers session
         */
        private int mOffererSessionId = 104;
        /**
         * id of the answerers session
         */
        private int mAnswererSessionId = 0;
        /**
         * tracks if ice candidates are all collected
         */
        boolean mIceReady = false;

        /**
         * create an observer for given stream
         */
        LicodeSdpObserver(StreamDescription stream, boolean publishing) {
            mStream = stream;
            mIsPublish = publishing;
            mSignalChannel = mIsPublish ? "publish" : "subscribe";
        }

        public boolean isLocal() {
            return mStream == null ? false : mStream.isLocal();
        }

        /**
         * waits for ice candidates to be gathered before triggering release
         */
        public void iceReady() {
            mIceReady = true;
            startConnecting();
        }

        private void startConnecting() {
            mStream.pc.createOffer(this, mStream.sdpConstraints());
        }

        @Override
        public void onCreateFailure(String arg0) {
            log("SdpObserver#onCreateFailure: " + arg0);
        }

        private SessionDescription modifySdpMaxBW(SessionDescription sdp) {
            StringBuffer desc = new StringBuffer();
            int audioLine = -1;
            int videoLine = -1;
            ArrayList<Integer> bLines = new ArrayList<Integer>();
            String[] lines = sdp.description.split("\r\n");
            for (int i = 0; i < lines.length; ++i) {
                if (lines[i].startsWith("m=audio")) {
                    audioLine = i;
                } else if (lines[i].startsWith("m=video")) {
                    videoLine = i;
                } else if (lines[i].startsWith("b=AS:")) {
                    bLines.add(i);
                }
            }
            // TODO dk: this may want to check for existing B-Lines!
            boolean addVideoB = mMaxVideoBW > 0;
            boolean addAudioB = mMaxAudioBW > 0;
            for (int i = 0; i < lines.length; ++i) {
                desc.append(lines[i]);
                desc.append("\r\n");
                if (i == audioLine && addAudioB) {
                    desc.append("b=AS:" + mMaxAudioBW + "\r\n");
                } else if (i == videoLine && addVideoB) {
                    desc.append("b=AS:" + mMaxVideoBW + "\r\n");
                }
            }

            return new SessionDescription(sdp.type, desc.toString());
        }

        @Override
        public void onCreateSuccess(SessionDescription sdp) {
            if (mLocalSdp != null) {
                return;
            }

            if (mIceReady) {
                mLocalSdp = sdp;
            }
            final SessionDescription finalSdp = modifySdpMaxBW(sdp);
            mActivity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mStream.pc.setLocalDescription(LicodeSdpObserver.this,
                            finalSdp);
                }
            });
        }

        @Override
        public void onSetFailure(String arg0) {
            log("SdpObserver#onSetFailure: " + arg0);
        }

        @Override
        public void onSetSuccess() {
            if (mLocalSdp == null) {
                return;
            }
            mActivity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (mStream.pc.getRemoteDescription() == null) {
                        sendLocalDescription();
                    } else {
                        // drain remote candidates?!
                        // also confirm exchange with licode server!
                        sendConfirmation();
                    }
                }
            });
        }

        void sendLocalDescription() {
            JSONObject desc = null;
            if (mIsPublish) {
                desc = mStream.toJsonOffer("offer");
            } else {
                desc = mStream.toJsonOffer(null);
                try {
                    desc.put("streamId", mStream.getId());
                } catch (JSONException e) {
                }
            }
            JSONObject p1 = new JSONObject();
            try {
                p1.put("messageType", "OFFER");
                p1.put("sdp", mLocalSdp.description);
                p1.put("tiebreaker",
                        (int) (Math.random() * (Integer.MAX_VALUE - 2)) + 1);
                p1.put("offererSessionId", mOffererSessionId); // hardcoded in
                // Licode?
                p1.put("seq", 1); // should not be hardcoded, but works for now
            } catch (JSONException e) {
            }
            log("SdpObserver#sendLocalDescription; to: " + mSignalChannel
                    + "; msg: " + p1.toString());
            sendSDPSocket(mSignalChannel, desc, p1, new Acknowledge() {
                @Override
                public void acknowledge(JSONArray arg0) {
                    log("SdpObserver#sendLocalDescription#sendSDPSocket#Acknowledge: "
                            + arg0.toString());

                    String streamId = null;
                    SessionDescription remoteSdp = null;
                    try {
                        // log(arg0.getString(0));
                        // JSONObject jsonAnswer = arg0.getJSONObject(0);
                        // licode server sends answer as string which is
                        // basically a json string, though
                        JSONObject jsonAnswer = new JSONObject(arg0
                                .getString(0));
                        boolean answer = "ANSWER".equals(jsonAnswer
                                .getString("messageType"));
                        if (!answer) {
                            log("SdpObserver: expected ANSWER, got: "
                                    + jsonAnswer.getString("messageType"));
                        }
                        remoteSdp = new SessionDescription(SessionDescription.Type.ANSWER,
                                jsonAnswer.getString("sdp"));

                        if (mIsPublish) {
                            streamId = arg0.getString(1);
                        }

                        mAnswererSessionId = jsonAnswer
                                .getInt("answererSessionId");
                    } catch (JSONException e1) {
                    }

                    if (mIsPublish) {
                        mStream.setId(streamId);
                        mLocalStream.put(streamId, mStream);
                    }

                    final SessionDescription finalRemoteSdp = remoteSdp;
                    mActivity.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mStream.pc.setRemoteDescription(
                                    LicodeSdpObserver.this, finalRemoteSdp);
                        }
                    });
                }
            });
        }

        void sendConfirmation() {
            JSONObject p0 = mStream.toJsonOffer("ok");
            try {
                p0.put("streamId", mStream.getId());
                p0.put("messageType", "OK");
                p0.put("offererSessionId", mOffererSessionId);
                p0.put("answererSessionId", mAnswererSessionId);
                p0.put("seq", 1);
                // p0.put("sdp", " ");
            } catch (JSONException e) {
            }
            sendSDPSocket(mSignalChannel, p0, p0, null);
        }
    }

    public MediaConstraints makePcConstraints() {
        MediaConstraints pcConstraints = new MediaConstraints();
        pcConstraints.optional.add(new MediaConstraints.KeyValuePair(
                "RtpDataChannels", "true"));
        pcConstraints.optional.add(new MediaConstraints.KeyValuePair(
                "EnableDtlsSrtp", "true"));
        pcConstraints.optional.add(new MediaConstraints.KeyValuePair(
                "DtlsSrtpKeyAgreement", "true"));
        return pcConstraints;
    }

    @Override
    public void publish(final VideoStreamsView view) {
        if (mPermissionPublish) {
            sVcHandler.post(new Runnable() {
                @Override
                public void run() {
                    doPublish(view);
                }
            });
        }
    }

    /**
     * begin streaming to server - MUST run on VcThread
     */
    void doPublish(VideoStreamsView view) {
        if (mVideoCapturer != null) {
            return;
        }

        MediaConstraints videoConstraints = new MediaConstraints();
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                "maxWidth", "320"));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                "maxHeight", "240"));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                "maxFrameRate", "10"));
        MediaConstraints audioConstraints = new MediaConstraints();
        audioConstraints.optional.add(new MediaConstraints.KeyValuePair(
                "googEchoCancellation2", "true"));
        audioConstraints.optional.add(new MediaConstraints.KeyValuePair(
                "googNoiseSuppression", "true"));
        lMS = sFactory.createLocalMediaStream("ARDAMS");

        if (videoConstraints != null) {
            mVideoCapturer = getVideoCapturer();
            mVideoSource = sFactory.createVideoSource(mVideoCapturer,
                    videoConstraints);
            VideoTrack videoTrack = sFactory.createVideoTrack("ARDAMSv0",
                    mVideoSource);
            lMS.addTrack(videoTrack);
        }
        if (audioConstraints != null) {
            AudioTrack audioTrack = sFactory.createAudioTrack("ARDAMSa0",
                    sFactory.createAudioSource(audioConstraints));
            lMS.addTrack(audioTrack);
            audioTrack.setEnabled(false);
        }

        StreamDescription stream = new StreamDescription("", false, true, true,
                false, null, mNick);
        MediaConstraints pcConstraints = makePcConstraints();
        MyPcObserver pcObs = new MyPcObserver(new LicodeSdpObserver(stream,
                true), stream);

        PeerConnection pc = sFactory.createPeerConnection(mIceServers,
                pcConstraints, pcObs);
        pc.addStream(lMS, new MediaConstraints());

        stream.setMedia(lMS);
        if (view != null) {
            stream.attachRenderer(new VideoCallbacks(view,
                    VideoStreamsView.LOCAL_STREAM_ID));
        }
        stream.initLocal(pc, pcObs.getSdpObserver());
    }

    @Override
    public void unpublish() {
        sVcHandler.post(new Runnable() {
            @Override
            public void run() {
                doUnpublish();
            }
        });
    }

    /**
     * stop all streams from being cast to the server
     */
    void doUnpublish() {
        for (String key : mLocalStream.keySet()) {
            final StreamDescription stream = mLocalStream.get(key);
            if (stream != null && stream.isLocal()) {
                stream.pc.removeStream(lMS);

                for (RoomObserver obs : mObservers) {
                    obs.onStreamRemoved(stream);
                }

                if (mObservers.size() == 0) {
                    destroy(stream);
                }
            }
        }
        mLocalStream.clear();

        if (lMS != null) {
            lMS.dispose();
        }
        if (mVideoCapturer != null) {
            mVideoCapturer.dispose();
        }

        lMS = null;
        mVideoCapturer = null;
        if (mVideoSource != null && !mVideoStopped) {
            mVideoSource.stop();
        }
        mVideoSource = null;
    }

    @Override
    public void subscribe(final StreamDescriptionInterface stream) {
        sVcHandler.post(new Runnable() {
            @Override
            public void run() {
                doSubscribe((StreamDescription) stream);
            }
        });
    }

    void doSubscribe(final StreamDescription stream) {
        if (stream.isLocal()) {
            return;
        }

        if (stream.getMedia() != null) {
            // already subscribed!
            triggerMediaAvailable(stream);
            return;
        }

        // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
        // NOTE: this _must_ happen while |factory| is alive!
        // Logging.enableTracing("logcat:",
        // EnumSet.of(Logging.TraceLevel.TRACE_ALL),
        // Logging.Severity.LS_SENSITIVE);

        MyPcObserver pcObs = new MyPcObserver(new LicodeSdpObserver(stream,
                false), stream);
        PeerConnection pc = sFactory.createPeerConnection(mIceServers,
                makePcConstraints(), pcObs);

        stream.initRemote(pc, pcObs.getSdpObserver());
    }

    /**
     * triggers the event that a stream was added - will eventually happen with
     * delay
     */
    void triggerStreamAdded(StreamDescription stream) {
        for (RoomObserver obs : mObservers) {
            obs.onStreamAdded(stream);
        }
    }

    /**
     * triggers the event that a stream was removed
     */
    void triggerStreamRemoved(StreamDescription stream) {
        for (RoomObserver obs : mObservers) {
            obs.onStreamRemoved(stream);
        }
        if (mObservers.size() == 0) {
            destroy(stream);
        }
    }

    /**
     * triggers the event that publish has been allowed now
     */
    void triggerPublishAllowed() {
        for (RoomObserver obs : mObservers) {
            obs.onPublishAllowed();
        }
    }

    /**
     * triggers that subscribe was successful, and media is now available to
     * stream
     */
    void triggerMediaAvailable(StreamDescriptionInterface stream) {
        for (RoomObserver obs : mObservers) {
            obs.onStreamMediaAvailable(stream);
        }
    }

    /**
     * triggers that a new video token is required - very soon - or the
     * connection will end
     */
    void triggerRequestVideoToken() {
        for (RoomObserver obs : mObservers) {
            obs.onRequestRefreshToken();
        }
    }

    @Override
    public void destroy(final StreamDescriptionInterface param0) {
        final StreamDescription stream = (StreamDescription) param0;
        if (stream == null) {
            return;
        }
        sVcHandler.post(new Runnable() {
            @Override
            public void run() {
                if (stream.pc != null) {
                    stream.pc.close();
                    stream.pc.dispose();
                }

                stream.onDestroyed();

                if (stream.isLocal()) {
                    sendMessageSocket("unpublish", stream.getId(), null);
                }
            }
        });
    }

    @Override
    public void disable(final StreamDescriptionInterface param0) {
        final StreamDescription stream = (StreamDescription) param0;
        if (stream.isLocal()) {
            return;
        }
        sVcHandler.post(new Runnable() {
            @Override
            public void run() {
                sendMessageSocket("unsubscribe", stream.getId(), null);
                stream.detachRenderer();

                stream.pc.close();
                stream.pc.dispose();
                stream.onDisable();
            }
        });
    }

    @Override
    public void setAudioEnabled(boolean enabled) {
        if (mState != State.kConnected || lMS == null) {
            return;
        }

        for (AudioTrack audioTrack : lMS.audioTracks) {
            audioTrack.setEnabled(enabled);
        }
    }

    @Override
    public void setActivity(Activity activity) {
        mActivity = activity;
    }

    @Override
    public Map<String, StreamDescriptionInterface> getRemoteStreams() {
        return mRemoteStream;
    }

    @Override
    public boolean isPublishing() {
        return mLocalStream.size() > 0;
    }

    @Override
    public void attachLocalStream(final VideoStreamsView vsv) {
        sVcHandler.post(new Runnable() {
            @Override
            public void run() {
                for (String key : mLocalStream.keySet()) {
                    StreamDescription stream = (StreamDescription) mLocalStream
                            .get(key);
                    stream.attachRenderer(new VideoCallbacks(vsv,
                            VideoStreamsView.LOCAL_STREAM_ID));
                    break;
                }
            }
        });
    }

    @Override
    public void detachLocalStream() {
        sVcHandler.post(new Runnable() {
            @Override
            public void run() {
                for (String key : mLocalStream.keySet()) {
                    StreamDescriptionInterface stream = mLocalStream.get(key);
                    if (stream != null) {
                        stream.detachRenderer();
                    }
                }
            }
        });
    }

    @Override
    public void post(Runnable r) {
        sVcHandler.post(r);
    }

    @Override
    public void attachRenderer(StreamDescriptionInterface stream,
                               VideoStreamsView mVsv) {
        ((StreamDescription) stream)
                .attachRenderer(new VideoCallbacks(mVsv, stream
                        .getId()));
    }

    @Override
    public void setNick(String nickname) {
        mNick = nickname;
    }

    @Override
    public boolean requestPublish() {
        if (mPermissionPublish) {
            sVcHandler.post(new Runnable() {
                @Override
                public void run() {
                    triggerPublishAllowed();
                }
            });
            return true;
        }
        return false;
    }

    /**
     * parse an acknowledge to a token sent, analyze for permissions, disconnect
     * on error
     */
    protected void parseVideoTokenResponse(JSONArray arg) {
        // TODO dk: parse all the other things that come with the response? TURN
        // Server, etc?
        boolean success = false;
        String message = "";
        try {
            success = "success".equalsIgnoreCase(arg.getString(0));
            if (success) {
                JSONObject obj = arg.getJSONObject(1);
                boolean subscribe = false;
                boolean publish = false;
                if (obj.has("permissions")) {
                    JSONObject permissions = obj.getJSONObject("permissions");
                    subscribe = permissions.has("subscribe")
                            && permissions.getBoolean("subscribe");
                    publish = permissions.has("publish")
                            && permissions.getBoolean("publish");
                }
                mPermissionSubscribe = subscribe;
                mPermissionPublish = publish;
            } else {
                message = arg.get(1).toString();
            }
        } catch (JSONException e) {
            log(e.getMessage());
        }

        if (!success) {
            log("Token failed: " + message);
            disconnect();
        }
    }
}
